/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License specifically applicable to
 * this software at http://developer.yahoo.com/yslow/license.txt, or the general
 * form at http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is: YSlow
 *
 * The Initial Developer of the Original Code is Yahoo! Inc.
 *
 * Copyright (C) 2007, Yahoo! Inc. All Rights Reserved.
 *
 * Contributor(s):
 * ***** END LICENSE BLOCK ***** */

//Peeler Class object in the YSLOW namespace.
YSLOW.Peeler = function(yslowContextObj) {
	//A Peeler object will always be associated with the corresponding YSlowContext object.
	this.yslowContextObj = yslowContextObj;
	this.excludelist = [ "yslow.user.js", "yslow-mini.js", "pencil-progbar-200x14.gif", "anim_ellipsis.gif" ];
    this.types = ["doc", "redirect", "iframe", "xhr", "flash", "js", "css", "cssimage", "image"];
    this.ignoreProtocols = ['data:', 'chrome:', 'javascript:', 'about:', 'resource:', 'jar:'];

    this.netPanelToYSlowMap =
    {
        "undefined": "undefined",
        html: "doc",
        css: "css",
        js: "js",
        xhr: "xhr",
        image: "image",
        flash: "flash",
        txt: "doc",
        bin: "bin"
    };

    this.mimeMap = {
        "text/plain" : "doc",
        "text/html" : "doc",
        "text/html" : "doc",
        "text/html" : "doc",
        "text/xml" : "doc",
        "text/css" : "css",
        "application/x-javascript" : "js",
        "text/javascript" : "js",
        "application/json" : "doc",
        "image/jpeg" : "image",
        "image/jpeg" : "image",
        "image/gif" : "image",
        "image/png" : "image",
        "image/bmp" : "image",
        "application/x-shockwave-flash" : "flash"
    };


}

// check if this is an allowed protocal (no chrome://, about: etc)
YSLOW.Peeler.prototype.isValidProtocol = function(s) {
    s = s.toLowerCase();
    for (var max = this.ignoreProtocols.length, i = 0; i < max; i++) {
        if (s.indexOf(this.ignoreProtocols[i]) === 0) {
            return false;
        }
    }
    return true;
}

// finds all components by looking at a/DOM and b/NetPanel
YSLOW.Peeler.prototype.findComponents = function() {
	this.yslowContextObj.comps = [];  // clear it out
	this.yslowContextObj.count = {};  // clear it out

	// Add a component for the actual HTML document so we can track the response time.
    var top_doc = this.yslowContextObj.document;
	var parentDocComp = new YSLOW.Component("doc", top_doc.location.href, this);
	parentDocComp.bParent = true;  // helps us distinguish the topmost doc from subdocs (frames, iframes, etc.)
	this.addComponent( parentDocComp );

    // compile a collection of all window objects (frames, iframes)
    var all_docs = this.findDocuments(this.yslowContextObj.document.defaultView);

    var prog_value = 0; // current value of the progress bar
    var increment = parseInt(100 / (this.yslowContextObj.comps.length * 5)) ; // 5 is the number of find*() functions we'll call

    // loop through all docs, frames, iframes and hunt components
    for(var i in all_docs) {
        this.yslowContextObj.document = all_docs[i].document;

        this.findStylesheets();
        YSLOW.Utils.setProgress(prog_value += increment);
        this.findScripts();
        YSLOW.Utils.setProgress(prog_value += increment);
        this.findFlash();
        YSLOW.Utils.setProgress(prog_value += increment);
        this.findCssImages();
        YSLOW.Utils.setProgress(prog_value += increment);
        this.findImages();
        YSLOW.Utils.setProgress(prog_value += increment);
    }

    this.yslowContextObj.document = top_doc; // back to the top parent
    this.findNetPanelComponentsAsync();
}


// Kick off the second phase of YSlow - getting details (headers, size, etc.) for each component.
YSLOW.Peeler.prototype.doneFinding = function() {

    this.sortComponents();
    YSLOW.Utils.setProgress(100);
    YSLOW.Utils.setBarNames("outerbar2", "progbar2");
	this.fetchComponentsDetails();
}

// recursive loop to find all frames (type "doc") and iframes (type "iframe")
// accepts a window object as a parameter and loops through the window's frames collection
// the function calls itself recursively passing every frame found
YSLOW.Peeler.prototype.findDocuments = function(win, docs) {

    var docs = docs || {};

    // check if frame digging was disabled
    // if so, return the top doc and exit
    if (!YSLOW.getPref("yslow.getFramesComponents", true)) {
        docs[win.location.href] = {'document': win.document};
        return docs;
    }

    var frames, which_type;
    if (win) {
        frames = win.frames;
        if (win.document) {
            if (this.isValidProtocol(win.document.location.href)) {
                which_type = 'doc';
                if (win.frameElement && win.frameElement.nodeName.toLowerCase() === 'iframe') {
                    which_type = 'iframe';
                }
                // addComponent, unless this is the top window doc
                // the top window doc already added before
                // findDocuments() was called
                if (win.top.location.href != win.location.href) {
                    this.addComponent( new YSLOW.Component(which_type, win.location.href, this) );
                }
                docs[win.location.href] = {'document': win.document};
            }
        }
        for (var i = 0; i < frames.length; i++) {
            this.findDocuments(frames[i], docs);
        }
    }
    return docs;
}


//YSlow Object
YSLOW.Peeler.prototype.findImages = function() {
    //dprint(9, "findImages: enter function");
	// body images: window.document.images.0.src
    var domobj = this.yslowContextObj.document.images;
	var hResults = {};
    for (var key in domobj) {
		if ( "object" == typeof(domobj[key]) ) {
			var valobj = domobj[key];
			if ( "string" == typeof(valobj["src"]) && 0 < valobj["src"].length ) {
 				var val = valobj["src"];
                if ( ! hResults[val] && this.isValidProtocol(val)) {
					hResults[val] = 1;
					this.addComponent( new YSLOW.Component("image", valobj["src"], this) );
				}
			}
		}
	}
}

//YSlow Object
YSLOW.Peeler.prototype.findCssImages = function() {
	var hUrls = {};
    var aElements = this.yslowContextObj.document.getElementsByTagName("*");
	for ( var i = 0; i < aElements.length; i++ ) {
		var sUrl = this.getCssBgImage(aElements[i], 'backgroundImage');
        if ( sUrl && this.isValidProtocol(sUrl) ) {
			hUrls[sUrl] = true;   // save them in a hash so we don't get dupes
		}

		sUrl = this.getCssBgImage(aElements[i], 'listStyleImage');
        if ( sUrl && this.isValidProtocol(sUrl) ) {
			hUrls[sUrl] = true;   // save them in a hash so we don't get dupes
		}
	}

	for ( var sUrl in hUrls ) {
		this.addComponent( new YSLOW.Component("cssimage", sUrl, this) );
	}
}


YSLOW.Peeler.prototype.getCssBgImage = function(elem, property) {
		var sUrl = YSLOW.Utils.getStyle(elem, property);
		if ( sUrl ) {
			var i1 = sUrl.indexOf("url(");
			var i2 = sUrl.lastIndexOf(")");
			if ( -1 != i1 && -1 != i2 && i1 < i2 ) {
				sUrl = sUrl.substring(i1+4, i2);
				sUrl = sUrl.replace(/\\,/g, ",");  // In FF for Y!Groups some of the URLs have "\," in them. FF is adding the backslash.
				sUrl = sUrl.replace(new RegExp("\"","g"), "");
				var sDisplay = YSLOW.Utils.getStyle(elem, "display");
				// Normally if an element is not displayed the background image is NOT downloaded, except in IE.
				// But we include it regardless because even if we're on FF we want to
				// alert developers to possible issues in IE.
				if ( "none" != sDisplay ) {
					// It DOES use a background image!
					return sUrl;
				}
			}
		}

		return undefined;
}


//YSlow Object
YSLOW.Peeler.prototype.findScripts = function() {
    //dprint(9, "findScripts: enter function");
    var aScripts = this.yslowContextObj.document.getElementsByTagName("script");
	for ( var i = 0; i < aScripts.length; i++ ) {
	    var valobj = aScripts[i];
		if ( "string" == typeof(valobj["src"]) && 0 < valobj["src"].length && this.isValidProtocol(valobj["src"]) ) {
			this.addComponent( new YSLOW.Component("js", valobj["src"], this) );
		}
	}
}

//YSlow Object
//TODO: This iteration logic is copied in lintExpression(),
// so changes should be mirrored across both functions.
//
YSLOW.Peeler.prototype.findStylesheets = function() {
    //dprint(9, "findStylesheets: enter function");

	// window.document.styleSheets.0.href
    var domobj = this.yslowContextObj.document.styleSheets;
    for (var key in domobj) {
		if ( "object" == typeof(domobj[key]) ) {
			var valobj = domobj[key];
            if ( "string" == typeof(valobj["href"]) && 0 < valobj["href"].length && this.isValidProtocol(valobj["href"]) ) {
                if ( valobj["href"] != this.yslowContextObj.document.location ) {
					// In FF *all* the stylesheets have an HREF field. It's set to the document.location.
					this.addComponent( new YSLOW.Component("css", valobj["href"], this) );
				}
				// If it IS an imported styled sheet we'll add the component inside this function.
				this.findImportedStyleSheets(valobj);
			}
		}
	}

	//TODO: This is IE ONLY. Remove it?
	// window.document.styleSheets.2.imports.0.href - IE only
    for (var key in domobj) {
		if ( "object" == typeof(domobj[key]) ) {
			var subobj = domobj[key];
			if ( "object" == typeof(subobj["imports"]) ) {
				var importObjects = subobj["imports"];
				// Iterate over all of the objects in "imports".
			    for (var key2 in importObjects) {
					if ( "object" == typeof(importObjects[key2]) ) {
						var valobj = importObjects[key2];
                        if ( "string" == typeof(valobj["href"]) && 0 < valobj["href"].length && this.isValidProtocol(valobj["href"]) ) {
							this.addComponent( new YSLOW.Component("css", valobj["href"], this) );
						}
					}
				}
			}
		}
	}
}

//YSlow Object
// Given a css rule, if it's an "@import" rule then add the style sheet component.
// Also, do a recursive check to see if this imported stylesheet itself contains an imported stylesheet.
YSLOW.Peeler.prototype.findImportedStyleSheets = function(styleSheet) {
	// In FF when document.domain is set in the original page we can't access cssRules.
	try {
		if ( "object" == typeof(styleSheet["cssRules"]) ) {
			for (var irule in styleSheet["cssRules"]) {
				if ( "length" == irule ) {
					continue;
				}
				var ruleobj = styleSheet["cssRules"][irule];
				if ( "object" == typeof(ruleobj["styleSheet"]) &&
                     "string" == typeof(ruleobj["styleSheet"]["href"]) && 0 < ruleobj["styleSheet"]["href"].length && this.isValidProtocol(ruleobj["styleSheet"]["href"]) ) {
					var impStyleSheet = ruleobj["styleSheet"];
					// It IS an imported stylesheet!
					this.addComponent( new YSLOW.Component("css", impStyleSheet["href"], this) );
					// Recursively check if this stylesheet itself imports any other stylesheets.
					this.findImportedStyleSheets(impStyleSheet);
				}
				else {
					break;
				}
			} // end for
		}
	} // end try
	catch(err) {
		eprint("Peeler.findImportedStyleSheets: Caught error finding imported stylesheets. Error: "+err.description);
	}
}

//YSlow Object
YSLOW.Peeler.prototype.findFlash = function() {
    //dprint(9, "findFlash: enter function");

	// flash objects: window.document.applets.swf_normal.Movie
	// ONLY WORKS IN IE!!!
    var domobj = this.yslowContextObj.document.applets;
    for (var key in domobj) {
		if ( "object" == typeof(domobj[key]) ) {
			var valobj = domobj[key];
			if ( "string" == typeof(valobj["Movie"]) && 0 < valobj["Movie"].length ) {
				this.addComponent( new YSLOW.Component("flash", valobj["Movie"], this) );
			}
		}
	}

	// ONLY WORKS IN FF!!!
    var aElements = this.yslowContextObj.document.getElementsByTagName("embed");
	for (var i = 0; i < aElements.length; i++) {
		var element = aElements[i];
        if ( "string" == typeof(element["src"]) && 0 < element.src.length && this.isValidProtocol(element.src) ) {
			this.addComponent( new YSLOW.Component("flash", element.src, this) );
		}
	}
}

// compile an array of existing componenets' URLs for dupe checks
YSLOW.Peeler.prototype.getUniqieComponentsList = function() {

    var found_len = this.yslowContextObj.comps.length;
    var found = [];
    for (var i = 0; i < found_len; i++) {
        found[this.yslowContextObj.comps[i].url] = 1;
    }
    return found;

}

// after we dom-inspected all the documents, let's take a look
// at what NetPanel has, so we can discover things such as
// XHRs and new Image().src that are not part of the DOM
YSLOW.Peeler.prototype.findNetPanelComponents = function() {

    // check if the user prefers to skip Net Panel components
    if (!YSLOW.getPref("yslow.getNetPanelComponents", true)) {
        this.doneFinding();
        return;
    }

    var found = this.getUniqieComponentsList();

    // loop through all entries in NetPanel
    var files = FirebugContext.netProgress.files;
    var nplen = files.length;
    var which_type;
    for (var i = 0; i < nplen; i++) {
        if (found[files[i].href]) continue; // we know about this file, skip the dupe
        if (!this.isValidProtocol(files[i].href)) continue; // not a valid protocol, skip

        // make sure the category is set
        // otherwise it's not if Netpanel
        // was not open
        FirebugContext.panelMap.net.getCategory(files[i]);

        // map Net Panel category to YSlow componenet type
        if (this.netPanelToYSlowMap[files[i].category]) {
            which_type = this.netPanelToYSlowMap[files[i].category];
        }
        // if the above fails, map content type to YSlow component type
        if ((!which_type || which_type === "undefined") && this.mimeMap[files[i].mimeType]) {
            which_type = this.mimeMap[files[i].mimeType];
        }
        // last resort catch-alls
        if (!which_type || which_type === "undefined") {
            if (files[i].mimeType && files[i].mimeType.indexOf('image/')) {
                which_type = 'image';
            }
        }
        if (!which_type || which_type === "undefined") {
            if (files[i].mimeType && files[i].mimeType.indexOf('text/')) {
                which_type = 'doc';
            }
        }
        this.addComponent( new YSLOW.Component(which_type, files[i].href, this) );
    }
	this.doneFinding();
}


//YSlow Object
// Make sure we don't include YSLOW components in the original window's components.
YSLOW.Peeler.prototype.excludeComponent = function(url) {

	for ( var i = 0; i < this.excludelist.length; i++ ) {
		if ( -1 != url.indexOf(this.excludelist[i]) ) {
			return true;
		}
	}
	return false;
}

//YSlow Peeler Object
//Keep a list of all of the components in the page.
YSLOW.Peeler.prototype.addComponent = function(compObj) {
    //dprint(9, "addComponent: enter function");
	// Don't add components that are part of YSlow? being inserted into the page.
	if ( this.excludeComponent(compObj.url) ) {
		return;
	}

	// Make sure it's a full URL path.
	if ( -1 == compObj.url.indexOf("/") ) {
		// There is NO path (not even relative), eg, "foo.gif".
		compObj.url = this.yslowContextObj.basehref + compObj.url;
	}
	else if ( "http" != compObj.url.substring(0,4) ) {
		if ( "/" == compObj.url.substring(0, 1) ) {
			// It's a relative path, eg, "/images/foo.gif".
			compObj.url = this.yslowContextObj.basehostname + compObj.url;
		}
		else {
			// It's a relative path, eg, "images/foo.gif".
			compObj.url = this.yslowContextObj.basehref + compObj.url;
		}
	}

	// id
	compObj.id = this.yslowContextObj.comps.length+1;

	this.incrCount(compObj.type);

	//Store the Component object in the YSlowContext comps hash.
	this.yslowContextObj.comps[this.yslowContextObj.comps.length] = compObj;
}

//YSlow Peeler
// Keep a running counter of each type of component.
YSLOW.Peeler.prototype.incrCount = function(sType) {
    //dprint(9, "YSLOW.Peeler.prototype.incrCount: enter function");

	if ( ! this.yslowContextObj.count[sType] ) {
		this.yslowContextObj.count[sType] = 0;
	}

	this.yslowContextObj.count[sType]++;

	return this.yslowContextObj.count[sType];
}

// callback for the sorting method
YSLOW.Peeler.prototype.componentSortCallback = function(a, b) {

    var sortBy = YSLOW.Peeler.prototype.sortBy;

    // are equal?
    if (a[sortBy] === b[sortBy]) {
        // secondary sorting by ID
        // to stabilize the sorting algo
        if (a.id > b.id) {
            return 1;
        }
        if (a.id < b.id) {
            return -1;
        }
        return 0;
    }

    // special case for sorting by type
    if (sortBy === 'type') {
        var types = FirebugContext.yslowContext.peelerObj.types;
        for (var i in types) {
            if (a.type == types[i]) {
                return -1;
            }
            if (b.type == types[i]) {
                return 1;
            }
        }
    }

    // special case - expires
    if (sortBy === 'expires') {
        var date_a = (a['expires']) ? new Date(a['expires']).getTime() : 0;
        var date_b = (b['expires']) ? new Date(b['expires']).getTime() : 0;
        if (date_a > date_b) {
            return 1;
        }
        if (date_a < date_b) {
            return -1;
        }
    }

    // normal comparison
    if (a[sortBy] > b[sortBy]) {
        return 1;
    }
    if (a[sortBy] < b[sortBy]) {
        return -1;
    }
    return 0;
}

// sorts components by specified key
YSLOW.Peeler.prototype.sortComponents = function(sortBy) {
    YSLOW.Peeler.prototype.sortBy = sortBy || 'type'; // sort by 'type' property by default
    FirebugContext.yslowContext.comps.sort(this.componentSortCallback);
}




/////////////// BEGIN AJAX

// Fetch the detailed information for all the components.
YSLOW.Peeler.prototype.fetchComponentsDetails = function() {
	this.yslowContextObj.fetchErrors = "";
	this.yslowContextObj.bPermDisabled = false;
	this.fetchNextComponentDetails();  // this will recursively call all the components in the page
}


// Fetch the detailed information for all the components.
YSLOW.Peeler.prototype.findNetPanelComponentsAsync = function() {

	// This is tricky: It's A LOT faster to get the component information (headers, size, etc.) from
	// Firebug's Net panel than it is to do our normal XHR of every component. Here, if the user
	// has not yet opened the Net panel, we activate it so that all the necessary information
	// (the netProgress.files array) is populated. Pros/cons:
	//   - without this YSlow cannot detect 301/302 redirects, since they don't show up in FF XHR
	//   - doing XHR gives better response time values
	if ( FirebugContext.netProgress && 0 == FirebugContext.netProgress.files.length ) {
		// Use setTimeout to give the panel time to draw the progress bars.
		FirebugContext.yslowContext.peelerObj.activateNetPanel();
	}
	else {
		this.findNetPanelComponents();  // this will recursively call all the components in the page
	}
}


// Fill the Net panel's info then proceed to fetch the components.
YSLOW.Peeler.prototype.activateNetPanel = function() {
	FirebugContext.netProgress.activate(FirebugContext.getPanel("net"));

	setTimeout("FirebugContext.yslowContext.peelerObj.findNetPanelComponents()", 1);  // this will recursively call all the components in the page
}


// Fetch the next component that has not yet had its details fetched.
YSLOW.Peeler.prototype.fetchNextComponentDetails = function() {
    //dprint(9, "YSLOW.Peeler.fetchNextComponentDetails: enter function");
	// Find the next component to fetch.
	var compObj = null, iFetched = 0;
	var aComponents = this.yslowContextObj.getComponents();
	for ( var i = 0; i < aComponents.length; i++ ) {
		if ( ! YSLOW.Component.getCompVal(aComponents[i], "fetch") ) {
 			compObj = aComponents[i];
			YSLOW.Component.setCompVal(compObj, "fetch", "start");  // set a flag to indicate we've (tried to) fetch this one
			break;
		}
		iFetched++;
	}

	// Update the progress bar.
	YSLOW.Utils.setProgress( (iFetched*100)/FirebugContext.yslowContext["comps"].length);

	if ( compObj ) {
		YSLOW.Utils.setProgressText(compObj['url']);
		compObj.fetchComponentDetails();
	}
	else {
		// No more URLs to fetch - we're done!
		YSLOW.Utils.setProgressText("Done");
		this.doneFetching();
	}
}

// end-of-fetching tasks
YSLOW.Peeler.prototype.doneFetching = function() {
	// Enable the Tools menu now that the page is peeled.
	FirebugChrome.window.document.getElementById('ysToolsMenu').disabled = false;
	FirebugContext.yslowContext.doView(FirebugContext.yslowContext.defaultview);
}

